/*
 * Copyright 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.compose.ui.input.rotary

import androidx.compose.ui.Modifier
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.InspectorInfo

/**
 * Adding this [modifier][Modifier] to the [modifier][Modifier] parameter of a component will allow
 * it to intercept [RotaryScrollEvent]s if it (or one of its children) is focused.
 *
 * @param onRotaryScrollEvent This callback is invoked when the user interacts with the rotary side
 *   button or the bezel on a wear device. While implementing this callback, return true to stop
 *   propagation of this event. If you return false, the event will be sent to this
 *   [onRotaryScrollEvent]'s parent.
 * @return true if the event is consumed, false otherwise.
 *
 * Here is an example of a scrollable container that scrolls in response to [RotaryScrollEvent]s.
 *
 * @sample androidx.compose.ui.samples.RotaryEventSample
 *
 * This sample demonstrates how a parent can add an [onRotaryScrollEvent] modifier to gain access to
 * a [RotaryScrollEvent] when a child does not consume it:
 *
 * @sample androidx.compose.ui.samples.PreRotaryEventSample
 */
fun Modifier.onRotaryScrollEvent(onRotaryScrollEvent: (RotaryScrollEvent) -> Boolean): Modifier =
    this then
        RotaryInputElement(onRotaryScrollEvent = onRotaryScrollEvent, onPreRotaryScrollEvent = null)

/**
 * Adding this [modifier][Modifier] to the [modifier][Modifier] parameter of a component will allow
 * it to intercept [RotaryScrollEvent]s if it (or one of its children) is focused.
 *
 * @param onPreRotaryScrollEvent This callback is invoked when the user interacts with the rotary
 *   button on a wear device. It gives ancestors of a focused component the chance to intercept a
 *   [RotaryScrollEvent].
 *
 * When the user rotates the side button on a wear device, a [RotaryScrollEvent] is sent to the
 * focused item. Before reaching the focused item, this event starts at the root composable, and
 * propagates down the hierarchy towards the focused item. It invokes any [onPreRotaryScrollEvent]s
 * it encounters on ancestors of the focused item. After reaching the focused item, the event
 * propagates up the hierarchy back towards the parent. It invokes any [onRotaryScrollEvent]s it
 * encounters on its way back.
 *
 * Return true to indicate that you consumed the event and want to stop propagation of this event.
 *
 * @return true if the event is consumed, false otherwise.
 * @sample androidx.compose.ui.samples.PreRotaryEventSample
 */
fun Modifier.onPreRotaryScrollEvent(
    onPreRotaryScrollEvent: (RotaryScrollEvent) -> Boolean
): Modifier =
    this then
        RotaryInputElement(
            onRotaryScrollEvent = null,
            onPreRotaryScrollEvent = onPreRotaryScrollEvent,
        )

private class RotaryInputElement(
    val onRotaryScrollEvent: ((RotaryScrollEvent) -> Boolean)?,
    val onPreRotaryScrollEvent: ((RotaryScrollEvent) -> Boolean)?,
) : ModifierNodeElement<RotaryInputNode>() {
    override fun create() =
        RotaryInputNode(onEvent = onRotaryScrollEvent, onPreEvent = onPreRotaryScrollEvent)

    override fun update(node: RotaryInputNode) {
        node.onEvent = onRotaryScrollEvent
        node.onPreEvent = onPreRotaryScrollEvent
    }

    override fun InspectorInfo.inspectableProperties() {
        onRotaryScrollEvent?.let {
            name = "onRotaryScrollEvent"
            properties["onRotaryScrollEvent"] = it
        }
        onPreRotaryScrollEvent?.let {
            name = "onPreRotaryScrollEvent"
            properties["onPreRotaryScrollEvent"] = it
        }
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is RotaryInputElement) return false

        if (onRotaryScrollEvent !== other.onRotaryScrollEvent) return false
        if (onPreRotaryScrollEvent !== other.onPreRotaryScrollEvent) return false

        return true
    }

    override fun hashCode(): Int {
        var result = onRotaryScrollEvent?.hashCode() ?: 0
        result = 31 * result + (onPreRotaryScrollEvent?.hashCode() ?: 0)
        return result
    }
}

private class RotaryInputNode(
    var onEvent: ((RotaryScrollEvent) -> Boolean)?,
    var onPreEvent: ((RotaryScrollEvent) -> Boolean)?,
) : RotaryInputModifierNode, Modifier.Node() {
    override fun onRotaryScrollEvent(event: RotaryScrollEvent) = onEvent?.invoke(event) ?: false

    override fun onPreRotaryScrollEvent(event: RotaryScrollEvent) =
        onPreEvent?.invoke(event) ?: false
}
